using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Diagnostics;
using Magnolia;
using System.Threading;
namespace CommLayer
{
    public class StringComTcpIp : StringComInt
    {
        protected class MessageQueueItem
        {
            public string m_message;
            public DateTime m_time;
            public MessageQueueItem(string message)
            {
                SetMessage(message);
            }

            public void SetMessage(string message)
            {
                m_message = message;
                m_time = DateTime.Now;
            }
        }
            


        string m_IPAddress;
        int m_port;
        TcpIpServer m_tcpServer;
        TcpIpClient m_tcpClient;
        protected Queue<MessageQueueItem> m_messageQueue = new Queue<MessageQueueItem>();
        protected delegate void SendStringAsyncDelegate(string xmlString);
        protected delegate void StringSenderDelegate();
        protected delegate void ConnectAsyncDelegate();

        protected bool m_firstConnectionEstablished = true;
        int m_lateMessagesDitch = 5;
        AutoResetEvent m_qEvent = new AutoResetEvent(false);

        /// <summary>
        /// Ip Address to call
        /// </summary>
        public override string IPAddress 
        {
            get {
                return m_IPAddress; 
            }
            set {
                m_IPAddress = value; 
            }
        }

        /// <summary>
        /// port to listen/call
        /// </summary>
        public override int Port
        {
            get { 
                return m_port; 
            } 
            set {
                m_port = value;
            }
        }

        public override bool IsClient
        {
            get
            {
                return m_tcpClient != null;
            }

        }

        public override bool IsConnected
        {
            get
            {
                return m_tcpClient != null && m_tcpClient.IsConnected;
            }

        }
 

        /// <summary>
        /// Internal string input call back function. Calls the notification delegate supplied at Initialization
        /// </summary>
        /// <param name="sRemoteAddress"></param>
        /// <param name="nRemotePort"></param>
        /// <param name="sData"></param>
        private void StringInputHandler(String sRemoteAddress, int nRemotePort, String sData)
        {
            DateTime arrivalTime = DateTime.Now;
            if (m_notificationDelegate!=null)
                m_notificationDelegate(arrivalTime,sData);
        }

        /// <summary>
        /// Calls the callback function
        /// </summary>
        /// <param name="sRemoteAddress"></param>
        /// <param name="nRemotePort"></param>
        /// <param name="sock"></param>
        private void ConnectionHandler(String sRemoteAddress, int nRemotePort, Socket sock)
        {
            Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceInfo,
                              "connected to " + sRemoteAddress + ":" + nRemotePort);
            // if we have (re)connected, signal the queue you can send its packages
            m_qEvent.Set();
            // and pass notification up
            if (m_notificationDelegateConnect!=null)
                m_notificationDelegateConnect(DateTime.Now,"connected to " + sRemoteAddress + ":" + nRemotePort);
        }

        /// <summary>
        /// Exception handler. Calls the callback function
        /// Added 11/27
        /// </summary>
        /// <param name="oBug"></param>
        private void ExceptionHandlerWithMemo(Exception oBug, string ID)
        {
            string strMsg = "exception for connection"+ID+":";
            strMsg += IPAddress.ToString() + ":" + Port.ToString();
            Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceInfo,
                  strMsg + oBug.Message);

            if (m_notificationDelegateException != null)
                m_notificationDelegateException(DateTime.Now, strMsg + oBug.Message);
        }


        /// <summary>
        /// Exception handler. Calls the callback function
        /// </summary>
        /// <param name="oBug"></param>
        private void ExceptionHandler(Exception oBug)
        {
            string strMsg = "exception for connection:";
            strMsg += IPAddress.ToString() + ":" + Port.ToString();
            Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceInfo,
                  strMsg + oBug.Message);

            if (m_notificationDelegateException != null)
                m_notificationDelegateException(DateTime.Now, strMsg + oBug.Message);
        }


        /// <summary>
        /// Ctor. initializes some defaults
        /// </summary>
        public StringComTcpIp()
        {
           
            IPAddress = "";
            Port = -1;
            m_tcpServer = null;
            m_tcpClient = null;
           
        }


        /// <summary>
        /// Initializes server and connect
        /// </summary>
        /// <param name="localIP"></param>
        /// <param name="port"></param>
        /// <param name="callbackFunc"></param>
        public override void  InitializeServer(DelegateStringComNotifyConnect connectCallbackFunc, 
                                                DelegateStringComNotifyData dataCallbackFunc, 
                                                DelegateStringComNotifyException exceptionCallbackFunc)
        {
            // connect trace
            if (AppContext.AppTraceSwitch == null)
                AppContext.AppTraceSwitch = new TraceSwitch("Magnolia", "Magnolia trace switch");

            // conenct delegates
            m_notificationDelegate = dataCallbackFunc;
            m_notificationDelegateException = exceptionCallbackFunc;
            m_notificationDelegateConnect = connectCallbackFunc;

            // must have a port. If address is not set we will take the first one from the dns
            if (Port < 0)
            {
                m_notificationDelegateException(DateTime.Now, "Initialize call should be preceeded by setting client IPAddress and Port");
                throw new Exception("Initialize call should be preceeded by setting client IPAddress and Port");
            }
  
            // construct server. first the call backs
            ConnectionFilterDelegate delegateConnectionFilter = new ConnectionFilterDelegate(ConnectionHandler);
            StringInputHandlerDelegate delegateStringInputHandler = new StringInputHandlerDelegate(StringInputHandler);
            ExceptionHandlerDelegate delegateExceptionHandler = new ExceptionHandlerDelegate(ExceptionHandler);
            // and then the server itself
            m_tcpServer = new TcpIpServerMedialon("",Port,
                7,12,// thread min/max
                1,0, // prefix/suffix
                delegateConnectionFilter, delegateStringInputHandler, delegateExceptionHandler// callbacks
                );

            m_tcpServer.InputProcessor = new InputProcessorMedialonServer(1,0,delegateStringInputHandler,(TcpIpServerMedialon)m_tcpServer);

            
            // start listening
            m_tcpServer.StartServer();

            // start the sender thread
            m_tcpServer.ThreadPool.InsertWorkItem("Send next string", new StringSenderDelegate(this.StringSender), null, false);

            Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceInfo, "InitializeServer done");
          }

        /// <summary>
        /// Initializes client and connect
        /// </summary>
        /// <param name="localIP"></param>
        /// <param name="port"></param>
        /// <param name="callbackFunc"></param>
        public override void InitializeClient(DelegateStringComNotifyConnect connectCallbackFunc, 
                                                DelegateStringComNotifyData dataCallbackFunc, 
                                                DelegateStringComNotifyException exceptionCallbackFunc)
        {
            // connect trace
            if (AppContext.AppTraceSwitch == null)
                AppContext.AppTraceSwitch = new TraceSwitch("Magnolia", "Magnolia trace switch");

            if (IsConnected)
                return;

            // conenct delegates
            m_notificationDelegate = dataCallbackFunc;
            m_notificationDelegateException = exceptionCallbackFunc;
            m_notificationDelegateConnect = connectCallbackFunc;
            
            // must have ip and port
            if (IPAddress == null || Port < 0)
            {
                m_notificationDelegateException(DateTime.Now, "Initialize call should be preceeded by setting client IPAddress and Port");
                throw new Exception("Initialize call should be preceeded by setting client IPAddress and Port");
            }

            // construct client
            ConnectionFilterDelegate delegateConnectionFilter = new ConnectionFilterDelegate(ConnectionHandler);
            StringInputHandlerDelegate delegateStringInputHandler = new StringInputHandlerDelegate(StringInputHandler);
            ExceptionHandlerDelegate delegateExceptionHandler = new ExceptionHandlerDelegate(ExceptionHandler);
            m_tcpClient = new TcpIpClientMedialon(m_IPAddress, Port, // address, pot
                                                    1,0,// pre/suffix
                                                    delegateConnectionFilter,delegateStringInputHandler,delegateExceptionHandler);

            m_tcpClient.InputProcessor = new InputProcessorMedialonClient(1, 0, delegateStringInputHandler, (TcpIpClientMedialon)m_tcpClient);

            // Try to connect. Need to consider a scenario of connect and send simultanously            // spawn a special thread to connect            
            ConnectAsync();

            // start the sender thread
            m_tcpClient.ThreadPool.InsertWorkItem("Send next string", new StringSenderDelegate(this.StringSender), null, false);

            m_lateMessagesDitch = 5;

        }

        public override int GetQueueSize()
        {
            int ret = 0;
            Monitor.Enter(m_messageQueue);
            ret = m_messageQueue.Count;
            Monitor.Exit(m_messageQueue);
            return ret;
        }

        /// <summary>
        /// Add a string to the queue for sending
        /// </summary>
        /// <param name="xmlString"></param>
        public override void QueueSendString(string xmlString)
        {
            // make a q item
            MessageQueueItem mqi = new MessageQueueItem(xmlString);

            // dont enqueue and send together
            try
            {
                Monitor.Enter(m_messageQueue);
                m_messageQueue.Enqueue(mqi);

            }
            catch (Exception e)
            {
                ExceptionHandlerWithMemo(e,"1");
                m_messageQueue.Clear();//Check here if comm fails
            }
            finally { Monitor.Exit(m_messageQueue); }

            // signal the other thread we are GO
            m_qEvent.Set();
        }

        /// <summary>
        /// Deal with NotConnected issues
        /// </summary>
        /// <returns></returns>
        bool DealNotConnected()
        {
            bool bSend = true;
            // If we are not connected on a client, we will try to connect here synchronously
            if (m_tcpClient != null && !m_tcpClient.IsConnected)
            {
                // but dont send yet
                bSend = false;
                // re-initialize connection if not doing it now
                if (!m_tcpClient.Connecting)
                    ConnectAsync();
                // bSend = m_tcpClient.Connect();// unremark for synchrounous reconnection 
                      
                // ditch all of the messages which are too late
                DitchLateMessages();
            }

            return bSend;
        }

        /// <summary>
        /// Takes the next string from the queue and sends it
        /// </summary>
        protected void StringSender()
        {
            bool InsideMonitor = false;
            while (true)
            {
                try
                {
                    // now we are sure we are connected
                    bool bSend = true;

                    // wait for a signal that something was added
                    m_qEvent.WaitOne();

                    
                    // see if reconnection is needed
                    bSend = DealNotConnected();
                    MessageQueueItem mqi;
                    // if all well, send whatever is in the queue
                    while (bSend && m_messageQueue.Count > 0)
                    {
                        // take from the queue
                        // lock the non-thread-safe q
                        InsideMonitor = true;
                        Monitor.Enter(m_messageQueue);
                        mqi = m_messageQueue.Dequeue();
                        Monitor.Exit(m_messageQueue);
                        InsideMonitor = false;
                        string xmlString = mqi.m_message;

                        // send the string. if it is disconnected, SendString will wait until connection
                        SendString(xmlString);

                        bSend = DealNotConnected();
                    }
                }
                catch (Exception oBug1)
                {
                    //Debug.Assert(false);
                    ExceptionHandlerWithMemo(oBug1,"2");
                }
                finally
                {
                    if(InsideMonitor)
                        Monitor.Exit(m_messageQueue);
                }
            }
        }

        /// <summary>
        /// Ditch messagees which are too late
        /// </summary>
        protected void DitchLateMessages()
        {
            int iCount = m_messageQueue.Count;
            iCount = Math.Max(iCount, 30);
            for (int i = 0; i < iCount; i++)
            {
                if (m_messageQueue.Count > 0)
                {        
                    // take from the queue (just look)
                    MessageQueueItem mqi = m_messageQueue.Peek();
                    //string xmlString = mqi.m_message;
                    TimeSpan diff = DateTime.Now - mqi.m_time;
                    // if too late, dequeue
                    if (diff.TotalSeconds > m_lateMessagesDitch) 
                    {
                        Monitor.Enter(m_messageQueue);
                            m_messageQueue.Dequeue();
                        Monitor.Exit(m_messageQueue);                        
                    }
                }
            }
        }


        /// <summary>
        /// send the string to the other side
        /// </summary>
        /// <param name="xmlString"></param>
        public override void SendString(String xmlString)
        {
            if (m_tcpClient != null)
            {
                if (!m_tcpClient.IsConnected)
                {
                    // re-initialize connection and process input
                    m_tcpClient.Connect();
                }

                if (m_tcpClient.IsConnected)
                {
                    m_tcpClient.SendStringData(xmlString);
                }
            }
            
            if (m_tcpServer != null)
            {
                m_tcpServer.SendStringData(xmlString);
                
            }
        }

        /// <summary>
        /// disconnect
        /// </summary>
        public override void Close()
        {
            // dont close and send together
            Monitor.Enter(this);

            if (m_tcpServer != null)
            {
                Monitor.Enter(m_tcpServer);
                m_tcpServer.StopServer();
                Monitor.Exit(m_tcpServer);
            }

            if (m_tcpClient != null)
            {
                m_tcpClient.ThreadPool.StopThreadPool();
                m_tcpClient.Reset();
            }

            Monitor.Exit(this);
        }

        #region SendAsync ()
        /// <summary>
        /// send the string to the other side
        /// </summary>
        /// <param name="xmlString"></param>
        public override void SendStringAsync(string xmlString)
        {
            // use one of the available threadpools
            if (m_tcpClient != null)
            {
                m_tcpClient.ThreadPool.InsertWorkItem("Send string async", new SendStringAsyncDelegate(this.BeginSendString), new Object[1] { xmlString }, false);
            }
            else if (m_tcpServer!=null)
            {
                m_tcpServer.ThreadPool.InsertWorkItem("Send string async", new SendStringAsyncDelegate(this.BeginSendString), new Object[1] { xmlString }, false);
            }
        }

        /// <summary>
        /// send the string to the other side asynchronously
        /// This is an event-type method, fire and forget. there is no callback for completion. However, on exception, the callback will be called
        /// </summary>
        /// <param name="xmlString"></param>
        protected void BeginSendString(String xmlString)
        {
            Monitor.Enter(this);

            if (m_tcpClient != null)
            {
                // regular send and re-connect, just on its own thread
                if (!m_tcpClient.IsConnected)
                {
                    // re-initialize connection and process input
                    m_tcpClient.Connect();
                }
  

                if (m_tcpClient.IsConnected)
                {
                    m_tcpClient.SendStringData(xmlString);
                }
            }
            
            if (m_tcpServer != null)
            {
                m_tcpServer.SendStringData(xmlString);

            }

            Monitor.Exit(this);

        }

        #endregion 

        /// <summary>
        /// connect async
        /// </summary>
        protected void ConnectAsync()
        {
            // connect only if not in midst of connecting right now
            if (!m_tcpClient.Connecting)
            {
                m_tcpClient.ThreadPool.InsertWorkItem("ConnectAsync", new ConnectAsyncDelegate(m_tcpClient.ConnectAsync), null, false);
            }
        }

    }
}
